package ogmios.core;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.io.*;
import java.util.*;
import java.util.List;
import javax.swing.text.*;
import javax.swing.tree.*;
import javax.swing.undo.*;
import javax.swing.event.*;
import ogmios.components.*;
import ogmios.dialogs.OgmiosFileChooser;
/**
 * OgmiosUtilities provides some common
 * functions to be used by classes within
 * Ogmios, but do not warrant an object
 * being created.
 */
public class OgmiosUtilities
{
  /**
   * Represents if the
   * last function succeeded.
   * See other OgmiosUtilties functions
   * for if they use this field.
   */
  public static boolean success;
  /**
   * Gets the platform specific new line character.
   */

  public static int MAX_UNDO_LEVEL = 1024;
  public static final String UNDOABLE_EDIT_LISTENER = "UndoableEditListener";
  public static final String UNDO_ACTION = "UndoAction";
  public static final String REDO_ACTION = "RedoAction";
  public static final String CLOSE_ACTION = "CloseAction";
  public static final Action CLOSE_ACTION_LISTENER = 
   new AbstractAction()
   {
    public void actionPerformed(ActionEvent e)
    {
      Component component;
      (component=(Component)e.getSource()).setVisible(false);
      component.validate();
      component.repaint();
    }
   };

  public static final Action CLOSE_WINDOW_ACTION_LISTENER = 
   new AbstractAction()
   {
    public void actionPerformed(ActionEvent e)
    {
      SwingUtilities.getWindowAncestor((Component)e.getSource()).setVisible(false);
    }
   };

  public static final String TAB_FORWARD_ACTION = "TabFoward";
  public static final Action TAB_FORWARD = 
  new AbstractAction()
  {
   public void actionPerformed(ActionEvent e)
   {
    JTabbedPane tabs = (JTabbedPane)e.getSource();
    int tabCount = tabs.getTabCount();
    int selectedIndex = tabs.getSelectedIndex();

    if (tabCount != 0)
     if (selectedIndex < tabCount - 1)
     {
       tabs.setSelectedIndex(selectedIndex + 1);
     }
     else
     {
       tabs.setSelectedIndex(0);
     }
   }
  };


  public static final String TAB_BACKWARD_ACTION = "TabBackward";
  public static final Action TAB_BACKWARD = 
  new AbstractAction()
  {
   public void actionPerformed(ActionEvent e)
   {
    JTabbedPane tabs = (JTabbedPane)e.getSource();
    int tabCount = tabs.getTabCount();
    int selectedIndex = tabs.getSelectedIndex();
    if (tabCount != 0)
     if (selectedIndex == 0)
     {
       tabs.setSelectedIndex(tabCount - 1);
     }
     else
     {
       tabs.setSelectedIndex(selectedIndex - 1);
     }
   }
  };

  /**
   * Adds a DefaultUndoableEditListener to the given document
   *
   *@param		document	The document that will have a
   *			DefaultUndoableEditListener added.
   *@return		The same Document given.
   */
  public static Document undoable(Document document)
  {
    UndoManager undo = new UndoManager();
    undo.setLimit(MAX_UNDO_LEVEL);
    document.putProperty(UNDOABLE_EDIT_LISTENER, undo);
    document.addUndoableEditListener(undo);
    return document;
  }
  /**
   * Adds a DefaultUndoableEditListener to the given JTextComponent's default document
   *
   *@param		text	The JTextComponent whose document will have a
   *			DefaultUndoableEditListener added, as well as Undo and Redo actions.
   *@return		The same JTextComponent given.
   */
  public static JTextComponent undoable(JTextComponent text)
  {
    undoable(text.getDocument());
    InputMap textInputMap = text.getInputMap(JComponent.WHEN_FOCUSED);
    ActionMap textActionMap = text.getActionMap();
    int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
    textInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, mask), UNDO_ACTION);
    textActionMap.put(UNDO_ACTION,
                      new AbstractAction()
    {
      public void actionPerformed(ActionEvent e)
      {
        UndoManager undo = ((UndoManager)((JTextComponent)e.getSource()).getDocument().getProperty(UNDOABLE_EDIT_LISTENER));
        if (undo.canUndo())
          undo.undo();
      }
    }
                     );
    textInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, mask), REDO_ACTION);
    textActionMap.put(REDO_ACTION,
                      new AbstractAction()
    {
      public void actionPerformed(ActionEvent e)
      {
        UndoManager undo = ((UndoManager)((JTextComponent)e.getSource()).getDocument().getProperty(UNDOABLE_EDIT_LISTENER));
        if (undo.canRedo())
          undo.redo();
      }
    }
                     );
    return text;
  }

 /**
 * Makes the component close when the user presses ESCAPE & the component is in the focused window.
 * 
 * @return     The component passed
 */
 public static JComponent closeable(JComponent component)
 {
  return closeable(component, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0));
 }

 /**
 * Makes the component close when the user invokes the key stroke & the component is in the focused window.
 * 
 * @return     The component passed
 */
 public static JComponent closeable(JComponent component, KeyStroke keyStroke)
 {
  return closeable(component, keyStroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
 }


 /**
 * Makes the component close when the user invokes the key stroke & the component meets the condition
 * 
 * @return     The component passed
 */
 public static JComponent closeable(JComponent component, KeyStroke keyStroke, int condition)
 {
  component.getInputMap(condition).put(keyStroke, CLOSE_ACTION);
  component.getActionMap().put(CLOSE_ACTION, CLOSE_ACTION_LISTENER);
  return component;
 }


 /**
 * Makes the component's Window close when the user presses ESCAPE & the component is in the focused window.
 * 
 * @return     The component passed
 */
 public static JComponent closeableChild(JComponent component)
 {
  return closeableChild(component, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0));
 }

 /**
 * Makes the component's Window close when the user invokes the key stroke & the component is in the focused window.
 * 
 * @return     The component passed
 */
 public static JComponent closeableChild(JComponent component, KeyStroke keyStroke)
 {
  return closeableChild(component, keyStroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
 }


 /**
 * Makes the component's Window close when the user invokes the key stroke & the component meets the condition
 * 
 * @return     The component passed
 */
 public static JComponent closeableChild(JComponent component, KeyStroke keyStroke, int condition)
 {
  component.getInputMap(condition).put(keyStroke, CLOSE_ACTION);
  component.getActionMap().put(CLOSE_ACTION, CLOSE_WINDOW_ACTION_LISTENER);
  return component;
 }
 
 /**
 * Makes the tabbed pane circulate through tabs with default modifier and the - = keys
 *
 * @return The component passed
 */
 public static JTabbedPane tabbable(JTabbedPane tabs)
 {
  int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
  return tabbable(tabs, KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, mask), KeyStroke.getKeyStroke(KeyEvent.VK_MINUS,mask));
 }

 /**
 * Makes the tabbed pane circulate through tabs with the passed keystrokes
 *
 * @return The component passed
 */
 public static JTabbedPane tabbable(JTabbedPane tabs, KeyStroke keyStrokeFoward, KeyStroke keyStrokeBackward)
 {
  return tabbable(tabs, keyStrokeFoward, keyStrokeBackward, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
 }
 

 /**
 * Makes the tabbed pane circulate through tabs with the passed keystrokes, given a certain condition
 *
 * @return The component passed
 */
 public static JTabbedPane tabbable(JTabbedPane tabs, KeyStroke keyStrokeForward, KeyStroke keyStrokeBackward, int condition)
 {
  InputMap inputMap =(tabs.getInputMap(condition));
  inputMap.put(keyStrokeForward, TAB_FORWARD_ACTION);
  ActionMap actionMap = (tabs.getActionMap());
  actionMap.put(TAB_FORWARD_ACTION, TAB_FORWARD);
  inputMap.put(keyStrokeBackward, TAB_BACKWARD_ACTION);
  actionMap.put(TAB_BACKWARD_ACTION, TAB_BACKWARD);
  return tabs;
 }

  /**
   * Returns the integer the String argument
   * represents, if it is one. Even if tryParse
   * is unsuccessful, it will return 0. Check
   * the success field for if the tryParse succeeded.
   * @param	input	The String that represents an integer
   */
  public static int tryParse(String input)
  {
    success = false;
    int len = input.length();
    if (len == 0)
     return 0;   
    int total = 0;
    boolean negative = false;

    int digit;
    char c;
    c = input.charAt(0);

    if (c == '-')
    {

      if (len == 1)
        return 0;

      negative = true;
      for (int i = 1; i < len; i++)
      {

        if ((digit = Character.digit(input.charAt(i), 10)) != -1)
          total = (total * 10) + digit;
        else
          return 0;


      }
      success = true;
      return -total;

    }

    else if ((total = Character.digit(c, 10)) != -1)
    {

      for (int i = 1; i < len; i++)
      {

        if ((digit = Character.digit(input.charAt(i), 10)) != -1)
          total = (total * 10) + digit;
        else
          return 0;


      }
      success = true;
      return total;

    }
    else
      return 0;



  }
  /**
   * Returns the first integer found inside the String
   * argument, if there is one. May return 0 even if
   * unsuccessful. Check the success field for if
   * the tryParseInside was successful.
   * @param	input	The String that has at least one integer inside
   */
  public static int tryParseInside(String input)
  {
    success = false;
    int len = input.length();
    if (len == 0)
     return 0;
    int total = 0;
    

    for (int i = 0; i < len; i++)
    {

      if ((total = Character.digit(input.charAt(i), 10)) != -1)
      {
        success = true;
        int digit;
        i++;
        for (; i < len; i++)
        {

          if ((digit = Character.digit(input.charAt(i), 10)) != -1)
            total = (total * 10) + digit;
          else
          {
            return total;
          }
        }
        return total;
      }

    }

    return 0;
  }
  /**
   * Returns the boolean the String represents, whether its "false" or "true".
   * May return false even if successful.
   * Check the success field for if
   * the tryParseBooleanPhrase was successful.
   */
  public static boolean tryParseBooleanPhrase(String input)
  {
    success = false;
    if (input.equals("false"))
    {
      success = true;
      return false;
    }
    if (input.equals("true"))
    {
      success = true;
      return true;
    }
    return false;
  }
  /**
   * Returns the boolean the String represents, whether its "0" or "1".
   * May return false even if successful.
   * Check the success field for if
   * the tryParseBooleanNumber was successful.
   */
  public static boolean tryParseBooleanNumber(String input)
  {
    success = false;
    if (input.equals("0"))
    {
      success = true;
      return false;
    }
    if (input.equals("1"))
    {
      success = true;
      return true;
    }
    return false;
  }
  /**
   * Returns the boolean the String represents, whether its "false", "true", "0", or "1".
   * May return false even if successful.
   * Check the success field for if
   * the tryParseBoolean was successful.
   */
  public static boolean tryParseBoolean(String input)
  {
    success = false;
    if (input.equals("0"))
    {
      success = true;
      return false;
    }
    if (input.equals("1"))
    {
      success = true;
      return true;
    }
    if (input.equals("false"))
    {
      success = true;
      return false;
    }
    if (input.equals("true"))
    {
      success = true;
      return true;
    }
    return false;
  }

  /**
   * Returns either the String "1" if the value is true,
   * or the String "0" if the value is false;
   */
  public static String booleanNumberString(boolean value)
  {
    return value ? "1" : "0";
  }

  /**
   * Returns the index of a component in a container,
   * or -1 if the component is not inside the container.
   * @param	box	The container to check
   * @param	item	The component to search for
   */
  public static int getComponentIndex(Container box, Component item)
  {
    for (int i = 0; i < box.getComponentCount(); i++)
      if (box.getComponent(i) == item)
        return i;
    return -1;
  }
  /**
   * Returns the index of a JMenuItem in a JMenu,
   * or -1 if the JMenuItem is not inside the container.
   * @param	menu		The JMenu to check
   * @param	item		The JMenuItem to search for
   */
  public static int getJMenuItemIndex(JMenu menu, JMenuItem item)
  {
    for (int i = 0; i < menu.getMenuComponentCount(); i++)
      if (menu.getMenuComponent(i) == item)
        return i;
    return -1;
  }

  /**
   * Returns the error information inside the Throwable
   * in a slightly more useful form.
   *
   * @param	ex	The Throwable to use
   */
  public static String getErrorMessage(Throwable ex)
  {
    StringWriter errors = new StringWriter();
    errors.write("Error Message: ");
    errors.write(ex.getMessage());
    errors.write("\n\n\nStack Trace:\n");
    ex.printStackTrace(new PrintWriter(errors));
    return errors.toString();
  }

  /**
   * Shows a message box with the given error message,
   * formatted to be friendlier to users and more useful to developers.
   *
   * @param	host	The Window to display the error box relative to
   * @param	ex	The Throwable to use
   * @param	title	The title of the error box
   */
  public static void showErrorBox(Component host, Throwable ex, String title)
  {
    JOptionPane.showMessageDialog(host, getTextBox(getErrorMessage(ex)), title, JOptionPane.ERROR_MESSAGE);
  }
  /**
   * Returns OgmiosUtilities' special textbox that is specially sized for the user's screen.
   *
   * @param	message		The message to place inside the textbox
   */
  public static JScrollPane getTextBox(String message)
  {
    OgmiosTextPane text = new OgmiosTextPane();
    JScrollPane textBox = new JScrollPane();
    text.setEditable(false);
    text.setOpaque(false);
    textBox.setOpaque(false);
    textBox.getViewport().setView(text);
    textBox.setBorder(null);
    text.setText(message);
    text.select(0, 0);
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    Dimension textSize = text.getPreferredSize();
    textBox.setPreferredSize(new Dimension(Math.min(screenSize.width / 3, textSize.width), Math.min(screenSize.height / 3, textSize.height)));
    return textBox;
  }

  /**
  * Returns OgmiosUtilities' special textbox that is specially sized for the user's screen
  *
  * @param     message     The message to place inside the textbox
  * @param     widthQuotient      The integer to divide the screen's width by
  * @param     heightQuotient      The integer to divide the screen's height by
  */
  public static JScrollPane getTextBox(String message, int widthQuotient, int heightQuotient)
  {
    OgmiosTextPane text = new OgmiosTextPane();
    JScrollPane textBox = new JScrollPane();
    text.setEditable(false);
    text.setOpaque(false);
    textBox.setOpaque(false);
    textBox.getViewport().setView(text);
    textBox.setBorder(null);
    text.setText(message);
    text.select(0, 0);
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    screenSize.width /= widthQuotient;
    screenSize.height /= heightQuotient;
    textBox.setPreferredSize(screenSize);
    return textBox;
  }

  /**
  * Returns input from user that possibly spans multiple lines
  *
  * @param     host     The window to show this dialog relative to
  * @param     message     The message to display along with this dialog
  * @param     title      The title of this dialog
  */
  public static String showMultiLineInputDialog(Component host, String message, String title)
  {
   JScrollPane textBox = getTextBox("", 3, 5);
   JTextComponent text = (JTextComponent)textBox.getViewport().getView();
   text.setEditable(true);
   text.setOpaque(true);
   undoable(text);
   textBox.setOpaque(true);
   if (JOptionPane.showOptionDialog(
       host, new Object[]{message, textBox}, title,
       JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
       null, null, null
       ) == JOptionPane.OK_OPTION)
     {
       return text.getText();
     }
    textBox.setOpaque(false);

    return null;
  }

  /**
   * Returns a single two-dimensional array representing the path.
   * The first index points to the directory, and the second index
   * points to the file name.
   *
   * @param	path	The path to split
   */
  public static String[] splitPath(String path)
  {
    int i = path.lastIndexOf(File.separator);
    if  (i != -1)
    {
      return new String[] {path.substring(0, i), path.substring(i + 1)};
    }
    else
    {
      return new String[] {"", path};
    }
  }

  /**
   * Returns the parent directory of a path.
   *
   * @param	path	The path to split
   */
  public static String getParentDirectory(String path)
  {
    int i = path.lastIndexOf(File.separator);
    return i == -1 ? "" : path.substring(0, i);
  }



  /**
   * Returns the filename of a path.
   *
   * @param	path	The path to split
   */
  public static String getFileName(String path)
  {
    int i = path.lastIndexOf(File.separator);
    return i == -1 ? path : path.substring(i + 1);
  }

  /**
  * Returns a single two-dimensional array representing the filename.
  * The first index points to the filename, and the second index
  * points to the extension.
  *
  * @param	fileName	The path to split
  */
 public static String[] splitFileName(String fileName)
 {
   int i = fileName.lastIndexOf(".");
   return i != -1 ?  new String[] {fileName.substring(0, i), fileName.substring(i + 1)} :
     new String[] {fileName, ""};
 }

  /**
   * Returns a filename without the last .xyz extension on it.
   *
   * @param	fileName	The filename to unextend.
   */
  public static String unextendedFileName(String fileName)
  {
    int i;
    return (i = fileName.lastIndexOf(".")) == -1 ? fileName : fileName.substring(0, i);
  }

  /**
   * Returns the extension of a filename
   *
   * @param	fileName	The filename to get the extension of.
   */
  public static String fileNameExtension(String fileName)
  {
    int i;
    return (i = fileName.lastIndexOf(".")) == -1 ? "" : fileName.substring(i+1);
  }

  /**
   * Returns the content of an input stream as a String
   *
   * @param	in	The input stream to read.
   */
  public static String inputStreamContents(InputStream in)
  {
    String line;
    StringBuilder contents;
    try
    {
      BufferedReader reader = new BufferedReader(new InputStreamReader(in));
      contents = new StringBuilder();
      while ((line = reader.readLine()) != null)
      {
        contents.append(line);
        contents.append("\n");
      }
      reader.close();
      return contents.substring(0, contents.length() - 1);
    }
    catch (Throwable ex)
    {
      return "";
    }
  }

  /**
   * Returns the contents of a file, with each line separated by \n
   *
   * @param	f	The file to read
   */
  public static String plainFileContents(File f)
  {
    String line = null;
    StringBuilder contents;
    try
    {
      BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(f)));
      contents = new StringBuilder();
      while ((line = reader.readLine()) != null)
      {
        contents.append(line);
        contents.append("\n");
      }
      reader.close();
      return contents.substring(0, contents.length() - 1);
    }
    catch (Throwable ex)
    {
      return "";
    }

  }

  /**
   * Returns the contents of a file directly.
   *
   * @param	f	The file to read
   */
  public static String fileContents(File f)
  {
    char[] buffer;
    int length;
    InputStream input;
    try
    {
      BufferedReader reader = new BufferedReader(new InputStreamReader(input = new FileInputStream(f)));
      length = input.available();
      reader.read(buffer = new char[length], 0, length);
      reader.close();
      return new String(buffer);
    }
    catch (Throwable ex)
    {
      return "";
    }

  }

 /**
 * Returns the contents of the file as a list of lines.
 */
 public static List<String> getLinesFromFile(File f)
 {
    String line = null;
    List<String> lines = new ArrayList<String>();
    try
    {
      BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(f)));
      while ((line = reader.readLine()) != null)
      {
        lines.add(line);
      }
      reader.close();
    }
    catch (Throwable ex)
    {
    }
    return lines;
 }

 /**
 * Returns the path with the package chopped off the end
 */
  public static String getBasePackagePath(File path, String packageName)
  {
   try
   {
    return getBasePackagePath(path.getCanonicalPath(), packageName);
   }
   catch(IOException ex)
   {
    showErrorBox(null, ex, "IO:Path Error");
    return "";
   }
  }
 /**
 * Returns the path with the package chopped off the end
 */
  public static String getBasePackagePath(String path, String packageName)
  {
   if (packageName.length() > 0)
   {
    if (packageName.endsWith("."))
     packageName = packageName.substring(0,packageName.length()-1);
    if (path.length()>1 && path.endsWith(File.separator))
     path = path.substring(0, path.length()-1);
    packageName = packageName.replace('.', File.separatorChar);
    if (path.endsWith(packageName))
     return path.substring(0, path.length() - packageName.length());
   }
   return path;

  }

  /**
  * Tries to have the text component to select the line
  *
  * @return     Whether the text component selected the line
  */
  public static boolean gotoLine(JTextComponent text, int line)
  {
   if (line > 0)
   {
   Element textLine = text.getDocument().getDefaultRootElement().getElement(line - 1);
    if (textLine != null)
    {
     text.grabFocus();
     text.select(textLine.getStartOffset() , textLine.getEndOffset() - 1);
     return true;
    }
   }
    return false;
   }
 

 /**
 * Gets a path to a node out of a tree based off an address, of the form of Root/Parent/Child...
 * Instead of /, use whatever the platform's file separator is.
 */
 public static TreeNode getNodeInTree(TreeModel tree, String address)
 {
   return getNodeInTree(tree, address.split(File.separatorChar == '\\' ? "\\\\" : File.separator));
 }

 /**
 * Gets a path to a node out of a tree based off an address, of the form of [Root,Parent,Child,...]
 */
 public static TreeNode getNodeInTree(TreeModel tree, String[] address)
 {
  if (address.length > 0)
  {
    Object root = tree.getRoot();
    if (root instanceof TreeNode && root.toString().equals(address[0]))
     return address.length == 1 ? (TreeNode)root : getNextNode((TreeNode)root, address, 1);
  }
   return null;
 }

 public static TreeNode getNextNode(TreeNode node, String[] address, int index)
 {
   String token = address[index++];
   if (index == address.length)
   {
     for (Enumeration children = node.children(); children.hasMoreElements();)
     {
       node = (TreeNode)children.nextElement();
       if (node.toString().equals(token))
          return node;       
      }
       return null;
    }
    for (Enumeration children = node.children(); children.hasMoreElements();)
    {
       node = (TreeNode)children.nextElement();
       if (node.toString().equals(token))
       {
         node = getNextNode(node, address, index);
          if (node != null)
           return node;
       }
    }
     return null;
 }

 /**
 * Returns either the absolute file of the child relative to the parent,
 * or the child itself, if the child is already absolute.
 */
 public static File resolvedNewFile(String child, String parent)
 {
  try
  {
   String parentPath = getParentDirectory(child);
   if (parentPath.length() > 0)
   {
    String childPath = parentPath + File.separator;
    if (new File(childPath).isAbsolute())
    {
      return new File(child).getCanonicalFile();
    }
   }
   return new File(parent, child).getCanonicalFile();
   }
   catch(Throwable ex)
   {

   }
   return new File("");
 }

 /**
 * Returns either the absolute file of the child relative to the parent,
 * or the child itself, if the child is already absolute.
 */
 public static String resolvedNewPath(String child, String parent)
 {
  return resolvedNewFile(child, parent).getPath();
 }

 /**
 * Returns either the absolute file of the child relative to the parent,
 * or the child itself. If neither exists, it will return new File("")
 */
 public static File resolvedFile(String child, String parent)
 {

   File cwd;

   if (parent.length() > 0)
   {
     try
    {
      cwd=new File(parent,child).getCanonicalFile();
      if (cwd.exists())
      return cwd;
     }
     catch(IOException ex)
    {

    }
   }
   try
  {
   cwd=new File(child);
   if (cwd.isAbsolute())
   {
     cwd = cwd.getCanonicalFile();
     if (cwd.exists())
      return cwd;
   }
  }
  catch(IOException ex)
  {

  }
  return new File("");
 }

 /**
 * Returns either the absolute path of the child relative to the parent,
 * or the child itself. If neither exists, it will return ""
 */
 public static String resolvedPath(String child, String parent)
 {
  return  resolvedFile(child, parent).getPath();
 }
 
}
